use self::models::*;
use hypers::tracing;
use hypers::{async_trait, hyper::StatusCode, prelude::*, Hook};
use std::sync::LazyLock;

static STORE: LazyLock<Db> = LazyLock::new(new_store);

pub struct MaxSize(pub u64);
#[async_trait]
impl Hook for MaxSize {
    async fn handle<'a>(&'a self, req: Request, next: Next<'a>) -> Response {
        use hypers::hyper::body::Body;
        let size_hint = req.body().size_hint().upper();
        println!("url = {}",req.uri().path());
        let res = next.next(req).await;
        if let Some(upper) = size_hint {
            if upper > self.0 {
                return res.render(StatusCode::PAYLOAD_TOO_LARGE);
            }
            return res;
        }
        res.render(StatusCode::BAD_REQUEST)
            .render("Request body size is unknown.")
    }
}

struct Api;
#[controller(name = "todos",hook = [MaxSize(1024)])]
impl Api {
    #[get("/")]
    async fn list_todos(req: &mut Request) -> impl Responder {
        let opts = req.parse_body::<ListOptions>().await?;
        let todos = STORE.lock().await;
        let todos: Vec<Todo> = todos
            .clone()
            .into_iter()
            .skip(opts.offset.unwrap_or(0))
            .take(opts.limit.unwrap_or(usize::MAX))
            .collect();
        let res = Response::default();
        Ok::<_, Error>(res.render(Json(todos)))
    }

    #[post("/")]
    async fn create_todo(req: &mut Request) -> impl Responder {
        let new_todo = req.parse_body::<Todo>().await?;
        tracing::debug!(todo = ?new_todo, "create todo");
        let mut vec = STORE.lock().await;
        let mut res = Response::default();
        for todo in vec.iter() {
            if todo.id == new_todo.id {
                tracing::debug!(id = ?new_todo.id, "todo already exists");
                res.status(StatusCode::BAD_REQUEST);
                break;
            }
        }
        vec.push(new_todo);
        res.status(StatusCode::CREATED);
        Ok::<_, Error>(res)
    }

    #[put("/:id")]
    async fn update_todo(req: &mut Request) -> Result<Response> {
        let id = req.param::<u64>("id")?;
        let updated_todo = req.parse_body::<Todo>().await?;
        tracing::debug!(todo = ?updated_todo, id = ?id, "update todo");
        let mut vec = STORE.lock().await;
        let mut res = Response::default();
        for todo in vec.iter_mut() {
            if todo.id == id {
                *todo = updated_todo;
                res.status(StatusCode::OK);
                break;
            }
        }
        tracing::debug!(?id, "todo is not found");
        res.status(StatusCode::NOT_FOUND);
        Ok(res)
    }

    #[delete("/:id")]
    async fn delete_todo(req: &mut Request) -> Result<Response> {
        let id = req.param::<u64>("id")?;
        tracing::debug!(?id, "delete todo");
        let mut vec = STORE.lock().await;
        let len = vec.len();
        vec.retain(|todo| todo.id != id);
        let mut res = Response::default();
        let deleted = vec.len() != len;
        if deleted {
            res.status(StatusCode::NO_CONTENT);
        } else {
            tracing::debug!(?id, "todo is not found");
            res.status(StatusCode::NOT_FOUND);
        }
        Ok(res)
    }
}

fn route() -> Router {
    Api.into()
}

async fn start_server() -> Result<()> {
    let root = route();
    // root.hook(MaxSize(1), None, None);
    tracing::info!("root = {:#?}", root);
    let listener = hypers::TcpListener::bind("127.0.0.1:7878").await?;
    hypers::listen(root, listener).await
}

#[tokio::main]
async fn main() -> Result<()> {
    tracing_subscriber::fmt().init();
    start_server().await
}

mod models {
    use serde::{Deserialize, Serialize};
    use tokio::sync::Mutex;

    pub type Db = Mutex<Vec<Todo>>;

    pub fn new_store() -> Db {
        Mutex::new(Vec::new())
    }

    #[derive(Serialize, Deserialize, Clone, Debug)]
    pub struct Todo {
        pub id: u64,
        pub text: String,
        pub completed: bool,
    }

    #[derive(Deserialize, Debug, Default)]
    pub struct ListOptions {
        pub offset: Option<usize>,
        pub limit: Option<usize>,
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use hypers::TestClient;

    #[tokio::test]
    async fn test() {
        tokio::task::spawn(async {
            let _ = start_server().await;
        });
        tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
        let cli = TestClient::new(route());
        let mut res = cli.post("http://127.0.0.1:7878/todos/")
            .json(&test_todo())
            .send()
            .await;
        assert_eq!(*res.status_mut(), StatusCode::CREATED);
        
        // let mut res = cli.post("http://127.0.0.1:7878/todos/")
        //     .form(&test_todo())
        //     .send()
        //     .await;
        // assert_eq!(*res.status_mut(), StatusCode::BAD_REQUEST);
    }

    fn test_todo() -> Todo {
        Todo {
            id: 1,
            text: "test todo".into(),
            completed: false,
        }
    }
}
